﻿namespace Okazuki.MVVM.PrismSupport.ViewModels.Utils
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Microsoft.Practices.Prism.Commands;

    /// <summary>
    /// ViewModelのリフレクションのためのユーテリティ
    /// </summary>
    internal static class ViewModelReflectionUtil
    {
        /// <summary>
        /// CanExecuteメソッドのプリフィックス
        /// </summary>
        private const string CommandCanExecuteMethodPrefix = "Can";

        /// <summary>
        /// CanExecuteメソッドのサフィックス
        /// </summary>
        private const string CommandExecuteMethodSufix = "Execute";

        /// <summary>
        /// void型が小さいと判定する比較クラス
        /// </summary>
        private class ReturnTypeCompare : IComparer<Type>
        {
            /// <summary>
            /// デフォルトインスタンス
            /// </summary>
            public static readonly ReturnTypeCompare Default = new ReturnTypeCompare();

            public int Compare(Type x, Type y)
            {
                if (x == y)
                {
                    return 0;
                }
                if (x == typeof(void))
                {
                    return -1;
                }
                return 1;
            }
        }

        /// <summary>
        /// 同期用のオブジェクト
        /// </summary>
        private static readonly object SyncObject = new object();

        /// <summary>
        /// 何度もリフレクションをしなくてもいいようにキャッシュを保持する
        /// </summary>
        private static readonly Dictionary<Type, ViewModelTypeInfo> Cache = new Dictionary<Type, ViewModelTypeInfo>();

        /// <summary>
        /// 指定した引数のViewModel型の解析情報を返す。一度解析したものはキャッシュしておいて次回からはキャッシュから返す。
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static ViewModelTypeInfo GetViewModelInfo(Type type)
        {
            if (Cache.ContainsKey(type))
            {
                return Cache[type];
            }

            return CreateAndRegisterViewModelTypeInfo(type);
        }

        /// <summary>
        /// 指定された型の解析を行いキャッシュに登録する
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static ViewModelTypeInfo CreateAndRegisterViewModelTypeInfo(Type type)
        {
            lock (SyncObject)
            {
                if (Cache.ContainsKey(type))
                {
                    return Cache[type];
                }
                var info = CreateViewModelTypeInfo(type);
                Cache.Add(type, info);
                return info;
            }
        }

        /// <summary>
        /// 型の解析を行う
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static ViewModelTypeInfo CreateViewModelTypeInfo(Type type)
        {
            var info = new ViewModelTypeInfo();
            // CommandMethod属性のついてるメソッドをコマンド名で纏める
            var commandMethods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                .Where(m => m.GetCustomAttributes(typeof(ViewModelBase.CommandMethodAttribute), true).Any())
                .OrderBy(m => m.ReturnType, ReturnTypeCompare.Default)
                .Select(m => new { Name = GetCommandPropertyName(m), Method = m })
                .Where(m => !string.IsNullOrWhiteSpace(m.Name))
                .GroupBy(m => m.Name)
                .ToDictionary(m => m.Key);

            // コマンド型のプロパティ
            var commandPropertyList = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(p => typeof(DelegateCommandBase).IsAssignableFrom(p.PropertyType))
                .ToList();
            // コマンド名でコマンドのプロパティを引けるように辞書化
            info.CommandProperties = commandPropertyList.ToDictionary(p => p.Name);

            // 自動初期化のコマンドプロパティを検出して辞書化
            var autoInitCommandProperties = commandPropertyList
                .Where(p => p.GetCustomAttributes(typeof(ViewModelBase.AutoInitCommandAttribute), true).Any())
                .ToDictionary(p => p.Name);

            // 自動初期化コマンドのコマンド情報を組み立てる
            info.AutoInitCommands =
                autoInitCommandProperties.SelectMany(p => commandMethods, (property, method) => new { property, method })
                // コマンドの名前とメソッドの名前が合わない物は除外
                .Where(p => p.property.Key == p.method.Key || p.property.Key == p.method.Key + "Command")
                // コマンド情報に変換
                .Select(p => new ViewModelCommandInfo
                {
                    CommandName = p.property.Key,
                    CommandProperty = p.property.Value,
                    ExecuteMethod = p.method.Value.ElementAt(0).Method,
                    CanExecuteMethod = p.method.Value.ElementAtOrDefault(1) == null ? null : p.method.Value.ElementAt(1).Method
                })
                .ToDictionary(p => p.CommandName);
            return info;
        }

        private static string GetCommandPropertyName(MethodInfo method)
        {
            var attr = method.GetCustomAttributes(typeof(ViewModelBase.CommandMethodAttribute), true)
                .OfType<ViewModelBase.CommandMethodAttribute>()
                .FirstOrDefault();
            if (attr == null)
            {
                return string.Empty;
            }

            if (!string.IsNullOrEmpty(attr.Name))
            {
                return attr.Name;
            }

            if (method.ReturnType == typeof(bool))
            {
                // CanExecute
                if (method.Name.StartsWith(CommandCanExecuteMethodPrefix)
                    && method.Name.EndsWith(CommandExecuteMethodSufix))
                {
                    return method.Name.Substring(
                        CommandCanExecuteMethodPrefix.Length,
                        method.Name.Length - CommandCanExecuteMethodPrefix.Length - CommandExecuteMethodSufix.Length);
                }
                else
                {
                    return string.Empty;
                }
            }
            else if (method.ReturnType == typeof(void))
            {
                // Execute
                if (method.Name.EndsWith(CommandExecuteMethodSufix))
                {
                    return method.Name.Substring(
                        0,
                        method.Name.Length - CommandExecuteMethodSufix.Length);
                }
                else
                {
                    return string.Empty;
                }
            }
            return string.Empty;
        }

    }
}
